<template>
	<div
		ref="containerRef"
		class="virtual-list-container"
		@scroll.passive="handleScroll"
	>
		<!-- 幽灵元素，用于撑开容器，形成正确的滚动条 -->
		<div
			class="virtual-list-phantom"
			:style="{ height: `${phantomHeight}px` }"
		></div>
		<!-- 真实渲染的列表区域 -->
		<div
			class="virtual-list-content"
			:style="{ transform: `translateY(${contentOffset}px)` }"
		>
			<div
				v-for="item in visibleData"
				:key="getKey(item.data)"
				class="virtual-list-item-wrapper"
				:ref="el => setItemRef(el, item.index)"
			>
				<!-- 使用作用域插槽，将item数据和index暴露给父组件 -->
				<slot :item="item.data" :index="item.index"></slot>
			</div>
		</div>
	</div>
</template>

<script setup lang="ts">
// --- 类型定义 ---
interface PositionInfo {
  index: number; // 索引
  height: number; // 高度
  top: number;    // 顶部距离
  bottom: number; // 底部距离
}

interface Props {
	listData: any[]; // 列表数据
	itemKey: string | ((item: any) => string | number); // 唯一键，对于v-for至关重要
	estimatedItemSize: number; // 预估项目高度，动态高度模式下必需
	buffer?: number; // 缓冲区项目数量，用于平滑滚动
}

// --- Props 定义 ---
const props = withDefaults(defineProps<Props>(), {
	buffer: 5, // 默认缓冲区为5项
});

// --- 插槽定义 (可选，为了更好的类型提示) ---
defineSlots<{
	default(props: { item: any; index: number }): any;
}>();

// --- DOM 引用 ---
// containerRef: 对应模板中的滚动容器。

// itemRefs:  Map，用来存储当前已渲染的DOM节点。
// key 是列表项的 index，value 是对应的 HTMLElement。
// 使用 Map 比普通对象更适合频繁增删的场景。

// setItemRef: 当一个元素被Vue挂载（渲染）时，el 是DOM元素，我们就把它存入Map。
// 当它被卸载（滚出可视区）时，Vue会用 null 来调用这个函数，我们正好可以从Map中把它删除，防止内存泄漏。
const containerRef = ref<HTMLDivElement | null>(null);
const itemRefs = ref<Map<number, HTMLElement>>(new Map());
const setItemRef = (el: Element | ComponentPublicInstance | null, index: number) => {
	if (el) {
		// 元素挂载时，添加到 Map 中
		itemRefs.value.set(index, el as HTMLElement);
	} else {
		// 元素卸载时（el 为 null），从 Map 中移除
		itemRefs.value.delete(index);
	}
};

// --- 核心状态 ---
const viewportHeight = ref(0); // 容器可视区域高度
const scrollTop = ref(0); // 滚动位置
// 存储每个项目的位置信息 整个组件最核心的数据结构 ，长度和 props.listData 一样。positions[i] 存储了 listData[i] 的精确位置信息
const positions = ref<PositionInfo[]>([]); 

// --- 初始化与监听 ---
onMounted(() => {
	if (!containerRef.value) return;
  // 获取容器高度
	viewportHeight.value = containerRef.value.clientHeight;
  // 初始化位置 预估
	initPositions();

	// 使用 ResizeObserver 监听容器尺寸变化
	const resizeObserver = new ResizeObserver(entries => {
		for (const entry of entries) {
			viewportHeight.value = entry.target.clientHeight;
		}
	});
	resizeObserver.observe(containerRef.value);

	onUnmounted(() => {
		resizeObserver.disconnect();
	});
});

// 监听数据变化，重新计算位置
watch(
	() => props.listData,
	() => {
		// 清空旧的DOM引用
		itemRefs.value.clear();
		// 重新初始化位置
		initPositions();
		// 数据变化可能导致滚动条消失，重置滚动位置
		if (containerRef.value) {
			containerRef.value.scrollTop = 0;
		}
		// 强制更新视图
		nextTick(() => {
			updateVisibleItems(containerRef.value?.scrollTop || 0);
		});
	},
	{ deep: true }
);

// 每次组件更新后，我们检查并更新渲染项的真实高度
onUpdated(() => {
  // 每次组件更新（DOM变化）后执行
	nextTick(() => {
		visibleData.value.forEach(item => {
			const domItem = itemRefs.value.get(item.index);
			if (!domItem) return;

			const { height } = domItem.getBoundingClientRect();
			const oldPosition = positions.value[item.index];
			const oldHeight = oldPosition.height;

			// 如果真实高度与预估高度不一致，则更新
			if (height > 0 && height !== oldHeight) {
				updateItemHeight(item.index, height);
			}
		});
	});
});

// --- 计算属性 ---

// 渲染的起始索引
const startIndex = ref(0);

// 渲染的结束索引 根据 startIndex 和可视区能容纳的数量计算得出
const endIndex = computed(() => {
	// 确保 endIndex 不会超过列表总长度
	return Math.min(
		startIndex.value + visibleCount.value + props.buffer,
		props.listData.length
	);
});



// 可视区域内可渲染的项目数量
const visibleCount = computed(() => {
	// 向上取整，确保能填满可视区域
	return Math.ceil(viewportHeight.value / props.estimatedItemSize);
});

// 幽灵元素的总高度
const phantomHeight = computed(() => {
	// 列表为空时高度为0
	if (positions.value.length === 0) return 0;
	// 否则为最后一项的底部位置
	return positions.value[positions.value.length - 1].bottom;
});

// 真实渲染内容的Y轴偏移量
const contentOffset = computed(() => {
	// 如果是列表开头，偏移量为0
	if (startIndex.value === 0 || positions.value.length === 0) return 0;
	// 否则为起始项的顶部位置
	return positions.value[startIndex.value].top;
});

// 真正需要渲染的数据
const visibleData = computed(() => {
	const start = startIndex.value;
	const end = endIndex.value;
	return props.listData.slice(start, end).map((data, index) => ({
		data,
		index: start + index,
	}));
});
// --- 方法 ---

// 获取列表项的唯一键
const getKey = (item: any): string | number => {
	if (typeof props.itemKey === "function") {
		return props.itemKey(item);
	}
	return item[props.itemKey];
};

// 初始化所有项目的位置信息
const initPositions = () => {
	const newPositions: PositionInfo[] = [];
	for (let i = 0; i < props.listData.length; i++) {
		newPositions.push({
			index: i,
			height: props.estimatedItemSize,
			top: i * props.estimatedItemSize,
			bottom: (i + 1) * props.estimatedItemSize,
		});
	}
	positions.value = newPositions;
};

// 更新单个项目的高度并重新计算后续项目的位置
const updateItemHeight = (index: number, newHeight: number) => {
	if (index < 0 || index >= positions.value.length) return;

	const pos = positions.value[index];
	const heightDifference = newHeight - pos.height;

	// 如果高度没变，则不进行任何操作
	if (Math.abs(heightDifference) < 1) return;

	pos.height = newHeight;
	pos.bottom = pos.top + newHeight;

	// 更新该项之后所有项的 top 和 bottom
	for (let i = index + 1; i < positions.value.length; i++) {
		positions.value[i].top = positions.value[i - 1].bottom;
		positions.value[i].bottom =
			positions.value[i].top + positions.value[i].height;
	}
};

// 使用二分查找来根据 scrollTop 找到起始索引
const findStartIndex = (scrollTopValue: number): number => {
	let low = 0;
	let high = positions.value.length - 1;
	let result = 0;

	while (low <= high) {
		const mid = Math.floor((low + high) / 2);
		const midPosition = positions.value[mid];

		if (
			midPosition.bottom >= scrollTopValue &&
			midPosition.top <= scrollTopValue
		) {
			result = mid;
			break;
		} else if (midPosition.bottom < scrollTopValue) {
			low = mid + 1;
		} else {
			high = mid - 1;
		}
	}
	// 为了平滑，可以从找到的位置往前推一个 buffer
	return Math.max(0, result - props.buffer);
};

// 滚动事件处理
const handleScroll = () => {
	if (!containerRef.value) return;
	const currentScrollTop = containerRef.value.scrollTop;
	updateVisibleItems(currentScrollTop);
};

// 更新可视区域的项目
const updateVisibleItems = (currentScrollTop: number) => {
	scrollTop.value = currentScrollTop;
	startIndex.value = findStartIndex(currentScrollTop);
};

// 在 VirtualList.vue 中暴露方法
const scrollToIndex = (index: number) => {
	if (!containerRef.value || index < 0 || index >= positions.value.length)
		return;
	const top = positions.value[index].top;
	containerRef.value.scrollTo({
		top: top,
		behavior: "smooth", // 使用平滑滚动
	});
};

defineExpose({
	scrollToIndex,
});
</script>

<style scoped>
.virtual-list-container {
	width: 100%;
	height: 100%;
	overflow-y: auto;
	position: relative;
	-webkit-overflow-scrolling: touch; /* 优化移动端滚动体验 */
}

.virtual-list-phantom {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	z-index: -1; /* 确保它在内容下方 */
}

.virtual-list-content {
	position: absolute;
	left: 0;
	top: 0;
	right: 0;
	/* 开启GPU加速，优化transform性能 */
	will-change: transform;
}

.virtual-list-item-wrapper {
	/* 确保wrapper填满内容，以便正确测量高度 */
	overflow: hidden;
}
</style>
